/*
 * Copyright 2011-2012 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.sabayframework.mem.impl.disk;

import java.io.File;
import java.io.IOException;
import java.util.TreeMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Manages data log files.
 * @author Alex Antonau
 * */
public final class DataLog {

	// Limitation of log space: 2^(MAX_SIZE_EXP+MAX_FILES_EXP)
	public static final int MAX_SIZE_EXP = DataEntry.MAX_SIZE_EXP;
	public static final int MAX_FILES_EXP = 9;//9 > DataEntry.MAX_FILES_EXP ? DataEntry.MAX_FILES_EXP : 9;
	public static final int MAX_NUMBER_OF_LOG_FILES = (1 << MAX_FILES_EXP)-1;
	
	public static final int DEFAULT_SIZE_EXP = 24; //16M
	public static final int DEFAULT_FILES_EXP = 4; //16
	
	private String workPath;
	private volatile int currentLogNumber;
	private TreeMap<Integer, DataLogFile> logs;
	private int logSizeExp = DEFAULT_SIZE_EXP;
	private int logFilesExp = DEFAULT_FILES_EXP;
		
	private static final Pattern filePattern = Pattern.compile("data-(\\d{1,})\\.log");
	
	public DataLog(String workPath) throws IOException {
		super();
		this.workPath = workPath;
		logs = new TreeMap<Integer, DataLogFile>();
		File path=new File(workPath);
		File[] files = path.listFiles();
		long maxFileSize = 0L;
		for(File f: files){
			if(f.isFile()){
				String name = f.getName();
				Matcher m =filePattern.matcher(name); 
				if(m.matches()){
					int n = Integer.parseInt(m.group(1));
					DataLogFile logFile = new DataLogFile(f, n);
					long size = logFile.getLength();
					if(size > maxFileSize) maxFileSize= size;
					logs.put(n, logFile);
				}
			}
		}
		if(logs.isEmpty()){
			createNew();
		}
		currentLogNumber = logs.lastKey();
		int filesNumber = logs.size();
		while( (1L << (logSizeExp+1)) < maxFileSize ) ++logSizeExp;
		while( (1 << logFilesExp) < filesNumber ) ++logFilesExp;
	}
	
	private void grow(){
		double ratio = 1.0*logSizeExp / logFilesExp;
		if(ratio >= 5.0) {
			//grow number of files
			if(logFilesExp < MAX_FILES_EXP) {
				logFilesExp++;
				return;
			}
		}
		//grow file size 
		if(logSizeExp < MAX_SIZE_EXP){
			logSizeExp++;
			return;
		}
		//fallback to number of files
		if(logFilesExp < MAX_FILES_EXP) logFilesExp++;
	}
	
	public int getMaxFilesNumber(){
		return 1 << logFilesExp;
	}
	public long getMaxFileSize(){
		return (1L << logSizeExp)-1;
	}
	public synchronized DataLogFile getLog(int number){
		return logs.get(number);
	}
	public synchronized int getFilesNumber(){
		return logs.size();
	}
	
	public DataLogFile[] getLogFiles(){
		return logs.values().toArray(new DataLogFile[logs.size()]);
	}
	
	public int getCurrentLogNumber() {
		return currentLogNumber;
	}
	/**
	 * Writes entry into a log file and updates entry attributes: logFile and logOffset.
	 * */
	public void writeData(DataEntry entry, byte[] data, int offset) throws IOException{
		checkCurrentLog();
		DataLogFile currentLog = getLog(currentLogNumber);
		long logOffset = currentLog.write(data, offset, entry.getLength());
		entry.setLogFile(currentLog.getNumber());
		entry.setLogOffset(logOffset);
	}
	
	public void flush() throws IOException{
		getLog(currentLogNumber).flush();
	}
	
	public byte[] readData(DataEntry entry) throws IOException{
		DataLogFile file = getLog(entry.getLogFile());
		return file.read(entry.getLogOffset(), entry.getLength());
	}
	public synchronized void deleteLog(int logNumber) throws IOException{
		DataLogFile log = logs.remove(logNumber);
		if(log!=null){
			log.close();
			log.getFile().delete();
			if(log.getNumber() == currentLogNumber){
				if(logs.isEmpty()) createNew();
				else currentLogNumber = logs.lastKey();
			}
		}
	}
	// creates new log if size exceeded current log file size 
	private synchronized void checkCurrentLog() throws IOException{
		DataLogFile currentLog = logs.get(currentLogNumber);
		if(currentLog.getLength() >= getMaxFileSize()){
			createNew();
		}
	}
	public synchronized DataLogFile createNew() throws IOException{
		if(logs.size() == MAX_NUMBER_OF_LOG_FILES)
			throw new IOException("Data log is out of space");
		if(logs.size() >= getMaxFilesNumber()-1) {
			grow();
		}
		DataLogFile currentLog = logs.get(currentLogNumber);
		if(currentLog !=null ) currentLog.makeReadOnly();
		int number = logs.isEmpty()? 1 : logs.lastKey()+1;
		String name = "data-" + String.valueOf(number)+".log";
		DataLogFile file=new DataLogFile(new File(workPath, name), number);
		logs.put(number, file);
		currentLogNumber =number;
		return file;
	}
	
	public synchronized void close(){
		for(DataLogFile f : logs.values()){
			try {
				f.close();
			} 
			catch (IOException e) {}
		}
	}
	public synchronized void delete(){
		for(DataLogFile f : logs.values()){
			try {
				f.close();
				f.getFile().delete();
			} 
			catch (IOException e) {}
		}
		logs.clear();
	}
	public long getFreeSpace() throws IOException {
		return getLog(currentLogNumber).getFile().getFreeSpace();
	}

}
